/*
 * Copyright 2009-2015 by WWW.KNET.CN  All rights reserved.
 */

package cn.knet.sp.ncrm.common.service;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.annotation.Resource;

import cn.knet.sp.ncrm.common.aspect.ExportLockAspect;
import cn.knet.sp.ncrm.common.utils.DateUtils;
import cn.knet.sp.ncrm.common.utils.Reflections;
import cn.knet.sp.ncrm.common.utils.TimeUtils;
import cn.knet.sp.ncrm.modules.sys.utils.ExcelUtils;


/**
 * 同步导出,减少内存消耗量,完全改动原有全部依赖于内存的导出表方式
 *
 * @author zhangbin
 * @version 2015 /05/06 10:29:41
 */
@Service
public class ExportSynService {

	private static final Logger logger = LoggerFactory.getLogger(ExportSynService.class);
	private static final int ACCESS_SIZE = 100;
	public static final String FOWAED_URL = "/export/synExcel.shtml";
	public static final String FOWAED_ERROR = "/export/synExportError.shtml";

	@Value("${adminPath}")
	private String adminPath;

	@Resource
	private ExportLockAspect exportLockAspect;

	/**
	 * 所机制,控制同一时间调用的对象个数 每次只能有一组对象被创建
	 */
	private final Lock lock = new ReentrantLock();



	/*********************************
	 * 对外接口片段
	 *********************************/

	/**
	 * 同步导出功能 本方法将被多次调用 当前环境下, 仅实现一套逻辑,让多人不会同时导出表单,而同时占用内存 增加强制锁减少 同一时间 因为导出报表占用内存过多 导致系统崩溃的情况
	 *
	 * @param baseSheet 自定义的excel导出格式 详见ExcelUtils对象
	 * @param model     当前请求的 model 部分系统自定义参数将在此被设 必须传入
	 * @return 重定向的一个url 这里固定为 forward:/export/synExcel.shtml 具体导出的逻辑将在这个连接被处理
	 * @author zhangbin
	 * @date 2015 /05/05 16:27:29
	 */
	public String exportSyn(ExcelUtils.Sheet baseSheet, Model model) {
		logger.info("报表导出文件start");
		try {
			lock.lock();
			logger.info("exportSyn 进行锁定!");
			String sheetName = createFileName(baseSheet.getName());
			File mkdir = new File(System.getProperty("java.io.tmpdir") + "/ncrmexport");
			if (!mkdir.exists()) {
				mkdir.mkdir();
			} else if (!mkdir.isDirectory()) {//若存在且为文件 则当前地址不可用
				model.addAttribute("state", false);
				model.addAttribute("message", "文件生成地址有误请联系管理员查询相关日志!");
				logger.error("can not create Directory{}", mkdir.getAbsoluteFile());
				return "forward:" + adminPath + FOWAED_URL; //发生错误则直接返回
			}
			File excelFile = new File(mkdir, UUID.randomUUID() + ".xlsx");
			FileOutputStream os = null;
			SXSSFWorkbook workbook = null;
			try {
				workbook = new SXSSFWorkbook(ACCESS_SIZE);
				os = new FileOutputStream(excelFile);
				Sheet sheet = workbook.createSheet();
				workbook.setSheetName(0, sheetName);
				settingSynWidth(sheet, baseSheet);
				settingSynHead(sheet, baseSheet, createCellHeadStyle(workbook));
				settingSynBody(sheet, baseSheet, createCellBodyStyle(workbook));
				workbook.write(os);
				model.addAttribute("state", true);
				model.addAttribute("file", excelFile);
				String fileName = createFileName(baseSheet.getName());
				fileName = new String(fileName.getBytes("GBK"), "ISO-8859-1");
				model.addAttribute("fileName", fileName);
			} catch (FileNotFoundException e) {
				model.addAttribute("state", false);
				model.addAttribute("message", "文件生成地址有误请联系管理员查询相关日志!");
				logger.warn("导出表单发生异常!文件找不到", e); //当前异常 通常不会发生 文件是新建的
			} catch (IOException e) {
				model.addAttribute("state", false);
				model.addAttribute("message", "写入文件发生异常!");
				relaseFile(excelFile);
				logger.error("写入文件发生异常!", e);
			} finally {
				relaseResource(os, workbook);
				logger.info("报表导出文件end");
			}
		} finally {
			lock.unlock();
			logger.info("exportSyn 解除锁定!");
		}
		return "forward:" + adminPath + FOWAED_URL;
	}

	/*********************************
	 * 资源处理片段
	 *********************************/

	/**
	 * 释放文件,将生成的临时文件删除,每次生成的文件名称是不同滴 当前方法的作用是在写文件的情况下,若发生异常 则 强制删除 关键点在于,当前文件可能稍后重定向是 就被使用 不能立即释放 因此单独抽取出来而不是放在final中
	 *
	 * @param excelFile 已经生成的excel文件
	 * @author zhangbin
	 * @date 2015 /05/13 17:52:23
	 */
	private void relaseFile(File excelFile) {
		try {
			if (excelFile != null && excelFile.exists()) {
				FileUtils.forceDelete(excelFile); //此处将文件进行强行删除
			}
		} catch (IOException e) {
			logger.warn("删除文件发生错误", e);
		}
	}

	/**
	 * 释放 文件流 excel导出流
	 *
	 * @param os       读取到文件流
	 * @param workbook 创建的 workbook对象
	 * @author zhangbin
	 * @date 2015 /05/14 14:33:51
	 */
	private void relaseResource(FileOutputStream os, SXSSFWorkbook workbook) {
		try {
			if (workbook != null) {
				workbook = null; //手工释放资源等待垃圾回收
			}
			if (os != null) {
				os.close();
				os = null;
			}
		} catch (IOException e) {
			logger.warn("导出excel表单 释放资源发生异常!", e);
		}
	}

	/*********************************
	 * excel处理片段
	 *********************************/

	/**
	 * excel创建每一行
	 *
	 * @param sheet     the sheet
	 * @param baseSheet the base sheet
	 * @param cellStyle the cell style
	 */
	private void settingSynBody(Sheet sheet, ExcelUtils.Sheet baseSheet, CellStyle cellStyle) {
		int rowIndex = 1; //第一行未开始位置
		for (Object bean : baseSheet.getList()) {
			Row row = sheet.createRow(rowIndex);
			int columnIndex = 0;
			if (baseSheet.isRows()) {
				Cell rowNumCell = row.createCell(0); //第一列设置为序列号
				rowNumCell.setCellValue(rowIndex); //设置当前行号
				rowNumCell.setCellStyle(cellStyle);
				columnIndex = 1;
			}
			for (ExcelUtils.Column column : baseSheet.getColumns()) {
				Cell cell = row.createCell(columnIndex);
				cell.setCellStyle(cellStyle);
				if (StringUtils.isNotBlank(column.getProperty())) {
					List<String> propertys = column.getPropertys(); //解析多层级属性
					String format = column.getFormat();//格式名称
					String style = column.getStyle();//格式配置
					if (propertys.size() == 1) { //如果是简单属性
						settingCell(propertys.get(0), bean, cell, format, style);
					} else if (propertys.size() > 1) { //注意 如果为0 则没调用直接设置为 空字符
						Object innerBean = bean; //初始化基础bean
						for (Iterator<String> it = propertys.iterator(); it.hasNext(); ) {
							String key = it.next();
							if (!it.hasNext()) { //若为最后一个
								settingCell(key, innerBean, cell, format, style);
							} else {
								innerBean = Reflections.invokeGetter(innerBean, key); //按照属性名称 反射
								if (innerBean == null) { //为空时，返回空字符串
									cell.setCellValue("");
									break;
								}
							}
						}
					} else {
						cell.setCellValue("");
					}
				}
				columnIndex++;
			}
			row = null;
			rowIndex++; //向下
		}
	}


	/**
	 * 为这个cell设置当前bean的属性 并安装format来进行格式化
	 *
	 * @param key    the key
	 * @param bean   the bean
	 * @param cell   the cell
	 * @param format the format
	 */
	private void settingCell(String key, Object bean, Cell cell, String format, String style) {
		Object value = null;
		Field field = null;
		try {
			value = Reflections.invokeGetter(bean, key); //调用key对应的的getter方法
			field = Reflections.getAccessibleField(bean, key); //获取对应的属性
			if (value != null) { //此处对特殊类型进行检测
				if (field != null ) { 
					if (field.getType().isEnum()) { //枚举类型 反射调用 getText方法
						Method enumMethod = field.getType().getDeclaredMethod("getText");
						if (enumMethod != null) {
							value = enumMethod.invoke(value);
						}
					} else if (field != null && field.getType().equals(Date.class)) { //是日期类型 转换为String
						if ("date".equals(format) && style != null) {
							value = DateUtils.formatDate((Date) value, style);
						} else {
							value = DateUtils.formatDate((Date) value);
						}
					}
				} else if (format != null) {
					if ("html".equals(format) && (value instanceof String)) {
						String newClob = value.toString().replace("</p><p>", "\n");
						newClob = newClob.replace("<p>", "").replace("</p>", "");
						value = newClob;
						cell.getCellStyle().setWrapText(true);
						cell.getCellStyle().setVerticalAlignment(CellStyle.VERTICAL_TOP);
					}
					if ("seconds-time".equals(format) && ((value instanceof Integer) || (value instanceof Long))) {
						long sec = (Long) value;
						if (StringUtils.isBlank(style)) {
							value = TimeUtils.toAllTimeString(sec, style);
						} else {
							value = TimeUtils.toAllTimeString(sec);
						}
					}
					if ("percent".equals(format) && (value instanceof Double)) {
						NumberFormat nf = NumberFormat.getPercentInstance();
						nf.setMinimumFractionDigits(2);//设置保留小数位
						nf.setRoundingMode(RoundingMode.HALF_UP); //设置舍入模式
						value = nf.format(value);
					}
				}
			}
			if (value != null) {
				settingCellValue(cell, value);
			} else {
				cell.setCellValue("");
			}
		} catch (NoSuchMethodException e) {//吃掉大部分异常 设置为空值
			e.printStackTrace();
			cell.setCellValue("");
		} catch (InvocationTargetException e) {
			e.printStackTrace();
			cell.setCellValue("");
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			cell.setCellValue("");
		} finally {
			if (cell != null) {
				cell = null; //手工释放资源
			}
			if (value != null) {
				value = null;
			}
			if (field != null) {
				field = null;
			}
		}
	}

	/**
	 * 具体设置相关属性
	 *
	 * @param cell  the cell
	 * @param value the value
	 */
	private void settingCellValue(Cell cell, Object value) {
		int classType = ExcelUtils.checkValueType(value);
		switch (classType) {
			case ExcelUtils.TEXT: {
				cell.setCellValue(ExcelUtils.convertText(value));
				break;
			}
			case ExcelUtils.UNDEFINE: {
				cell.setCellValue(ExcelUtils.convertAll(value));
				break;
			}
			case ExcelUtils.FLOAT: {
				cell.setCellValue(ExcelUtils.convertFLOAT(value));
				break;
			}
			case ExcelUtils.INTEGER: {
				cell.setCellValue(ExcelUtils.convertInteger(value));
				break;
			}
			default: {
				cell.setCellValue(ExcelUtils.convertAll(value));
				break;
			}
		}
	}

	/**
	 * 创建表头
	 *
	 * @param sheet     the sheet
	 * @param baseSheet the base sheet
	 * @param cellStyle the cell style
	 */
	private void settingSynHead(Sheet sheet, ExcelUtils.Sheet baseSheet, CellStyle cellStyle) {
		Row row = sheet.createRow(0); //创建第1行
		int index = 0;
		if (baseSheet.isRows()) {
			Cell cell = row.createCell(0); //设置第一列为序号
			cell.setCellValue("序号");
			cell.setCellStyle(cellStyle);
			index = 1;
		}
		for (String title : baseSheet.getTitles()) {
			Cell cell = row.createCell(index);
			cell.setCellValue(title);
			cell.setCellStyle(cellStyle);
			index++;
		}
	}

	/**
	 * 创建体的字体等
	 *
	 * @param wb the wb
	 * @return the cell style
	 * @author zhangbin
	 * @date 2015 /05/06 13:53:03
	 */
	private CellStyle createCellBodyStyle(Workbook wb) {
		CellStyle cellStyle = wb.createCellStyle();
		Font font = wb.createFont();
		font.setFontHeight((short) 200);
		font.setCharSet(Font.ANSI_CHARSET);
		cellStyle.setFont(font);
		cellStyle.setWrapText(true); //设置自适应行高
		return cellStyle;
	}

	/**
	 * 创建基础的样式 上下居中对齐,并自适应行高
	 *
	 * @param wb the wb
	 * @return the cell style
	 * @author zhangbin
	 * @date 2015 /05/06 11:17:07
	 */
	private CellStyle createCellHeadStyle(Workbook wb) {
		CellStyle cellStyle = wb.createCellStyle();
		Font font = wb.createFont();
		font.setFontHeight((short) 200);
		font.setBoldweight(Font.BOLDWEIGHT_BOLD);
		cellStyle.setFont(font);
		cellStyle.setAlignment(CellStyle.ALIGN_CENTER);
		cellStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
		cellStyle.setWrapText(true); //设置自适应行高
		return cellStyle;
	}

	/**
	 * 设置每一列的宽度 通常作用是设置宽度 以满足 备注 和 联系人 信息过长的情况 需要改进
	 *
	 * @param sheet     the sheet
	 * @param baseSheet the base sheet
	 */
	private void settingSynWidth(Sheet sheet, ExcelUtils.Sheet baseSheet) {
		List<String> titles = baseSheet.getTitles();
		int index = 0;//初始化从0开始
		int widthSizeCommon = 3000;
		int widthSizeLager = 8000;
		if (baseSheet.isRows()) {
			sheet.setColumnWidth(0, widthSizeCommon);
			index = 1;
		}

		for (ExcelUtils.Column column : baseSheet.getColumns()) {
			if (column.getWidth() > -1) {
				sheet.setColumnWidth(index, column.getWidth());
			} else {
				if (column.getTitle().contains("备注") || column.getTitle().contains("联系人")) {
					sheet.setColumnWidth(index, widthSizeLager);
				} else {
					sheet.setColumnWidth(index, widthSizeCommon);
				}
			}
			index++;
		}
	}

	/**
	 * 生成简单的表单名称
	 *
	 * @param name the name
	 * @return the string
	 * @author zhangbin
	 * @date 2015 /05/06 10:32:33
	 */
	private String createFileName(String name) {
		String fileName = name + "-" + new java.sql.Date(new Date().getTime()).toString();
		return fileName;
	}

	/**
	 * 根据同步注解 将一个任意对象写入到内存,以便之后使用
	 * @author zhangbin
	 * @date 2015 /09/23 15:21:55
	 * @param o the o
	 */
	public void putValue(String id, Object o) {
		exportLockAspect.putStateObject(id, o);
	}
}
